package com.leonzhangxf.ioc;

import com.google.common.collect.Sets;
import com.leonzhangxf.ioc.transaction.ProxyFactory;
import com.leonzhangxf.ioc.transaction.TransactionManager;
import com.leonzhangxf.ioc.transaction.Transactional;
import com.leonzhangxf.ioc.util.CollectionUtils;
import lombok.Value;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @author leonzhangxf
 */
public class BeanFactoryImpl implements BeanFactory, BeanDefinitionRegistry {

    private Set<BeanDefinition> beanDefinitions;

    private Set<BeanWrapper> beanWrappers = Sets.newHashSet();

    private Map<String, Object> idBeans;
    private Map<Class<?>, Object> typeBeans;

    public Set<BeanDefinition> getBeanDefinitions() {
        if (CollectionUtils.isEmpty(this.beanDefinitions)) {
            this.beanDefinitions = Sets.newConcurrentHashSet();
        }
        return beanDefinitions;
    }

    @Override
    public synchronized void registerBeanDefinition(BeanDefinition beanDefinition) {
        getBeanDefinitions().add(beanDefinition);
    }

    @Override
    public synchronized void registerBeanDefinitions(Set<? extends BeanDefinition> beanDefinitions) {
        getBeanDefinitions().addAll(beanDefinitions);
    }

    @Override
    public synchronized void initializeBeans() throws Exception {
        Set<BeanDefinition> beanDefinitions = getBeanDefinitions();

        // factory create bean and resolve deps.
        for (BeanDefinition bd : beanDefinitions) {
            if (beanWrappers.stream().anyMatch(ele -> ele.getId().equals(bd.getId()))) {
                continue;
            }
            BeanWrapper beanWrapper = initializeBeanWithoutDepsInject(bd);
            beanWrappers.add(beanWrapper);

            resolveDeps(beanDefinitions, bd, beanWrapper);
        }

        // post process, now just for transaction support.
        postProcessBeans();

        // resolve bean container
        assignBeanMap();
    }

    private void assignBeanMap() {
        this.idBeans = this.beanWrappers.stream()
            .collect(Collectors.toMap(BeanWrapper::getId, BeanWrapper::getBean));
        this.typeBeans = this.beanWrappers.stream()
            .map(wrapper -> {
                Class<?> type = wrapper.getType();
                Set<Class<?>> interfaces = wrapper.getBeanDefinition().getInterfaces();
                HashSet<Class<?>> classes = Sets.newHashSet(interfaces);
                classes.add(type);
                return classes.stream().map(clazz -> new AllTypeBeanWrapper(clazz, wrapper.getBean()))
                    .collect(Collectors.toList());
            }).flatMap(Collection::stream)
            .collect(Collectors.toMap(AllTypeBeanWrapper::getType, AllTypeBeanWrapper::getBean));
    }

    @Value
    static class AllTypeBeanWrapper {
        Class<?> type;
        Object bean;
    }

    private void resolveDeps(Set<BeanDefinition> beanDefinitions, BeanDefinition bd,
                             BeanWrapper beanWrapper) throws Exception {
        Set<Dependency> dependencies = bd.getDependencies();
        if (!CollectionUtils.isEmpty(dependencies)) {
            for (Dependency dep : dependencies) {
                BeanWrapper depWrapper;

                Optional<BeanWrapper> existDepOpt = beanWrappers.stream()
                    .filter(bean -> dep.getDepsClass().isAssignableFrom(bean.getType())).findAny();
                if (existDepOpt.isPresent()) {
                    // dep already created
                    depWrapper = existDepOpt.get();
                } else {
                    // dep not bean created yet.
                    Optional<BeanDefinition> anyDepBdOpt = beanDefinitions.stream()
                        .filter(depBd -> dep.getDepsClass().isAssignableFrom(depBd.getType())).findAny();
                    if (anyDepBdOpt.isEmpty()) {
                        throw new RuntimeException(
                            String.format("deps could not be found. bd: %s, dep: %s",
                                bd, dep)
                        );
                    }
                    BeanDefinition depBd = anyDepBdOpt.get();
                    depWrapper = initializeBeanWithoutDepsInject(depBd);
                    beanWrappers.add(depWrapper);
                    resolveDeps(beanDefinitions, depBd, depWrapper);
                }

                // inject
                Object depBean = depWrapper.getBean();
                Method method = dep.getMethod();
                method.setAccessible(true);
                method.invoke(beanWrapper.getBean(), depBean);
            }
        }
    }

    private void postProcessBeans() {
        // transaction support.
        Optional<BeanWrapper> transactionManagerOpt = beanWrappers.stream()
            .filter(beanWrapper -> TransactionManager.class.isAssignableFrom(beanWrapper.getType()))
            .findAny();
        if (transactionManagerOpt.isEmpty()) {
            System.out.println("would not be able to support transaction");
            return;
        }
        TransactionManager transactionManager = (TransactionManager) transactionManagerOpt.get().getBean();
        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.setTransactionManager(transactionManager);

        // reset the bean wrapper bean as proxy.
        beanWrappers.stream().filter(wrappers -> Arrays.stream(wrappers.getType().getMethods())
            .anyMatch(method -> null != method.getAnnotation(Transactional.class)))
            .forEach(wrapper -> {
                Object bean = wrapper.getBean();
                Set<Class<?>> interfaces = wrapper.getBeanDefinition().getInterfaces();
                // has interfaces or not
                Object proxy;
                if (CollectionUtils.isEmpty(interfaces)) {
                    proxy = proxyFactory.getCglibProxy(bean);
                } else {
                    proxy = proxyFactory.getJdkProxy(bean);
                }
                wrapper.setBean(proxy);
            });
    }

    BeanWrapper initializeBeanWithoutDepsInject(BeanDefinition bd) throws Exception {
        Class<?> type = bd.getType();
        Constructor<?> constructor = type.getDeclaredConstructor();
        constructor.setAccessible(true);
        Object instance = constructor.newInstance();

        BeanWrapper beanWrapper = new BeanWrapper();
        beanWrapper.setBean(instance);
        beanWrapper.setType(type);
        beanWrapper.setBeanDefinition(bd);
        return beanWrapper;
    }

    @Override
    public Object getBean(String id) {
        return idBeans.get(id);
    }

    @Override
    public <T> T getBean(Class<T> type) {
        return (T) typeBeans.get(type);
    }
}
